Micron Document
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| SparkN0de-git | SparkN0de |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


Commit 955263fceba6d1e792f650960719ffee83bfb949


Parents : 0999943
Author : Ivan <ivan@quad4.io>
Signature : Invalid signer <e46112d44649266d71fe2193e00a4710>, author is <ivan@quad4.io>
Date : 2026-06-19T10:24:14-05:00

feat(download): update file download functionality by implementing DownloadUtils for API responses, improving filename parsing, and updating various components to utilize the new utility methods

Changes
Diff

diff --git a/meshchatx/src/frontend/components/about/AboutPage.vue b/meshchatx/src/frontend/components/about/AboutPage.vue
index e24c7169..dcd40bcb 100644
--- a/meshchatx/src/frontend/components/about/AboutPage.vue
+++ b/meshchatx/src/frontend/components/about/AboutPage.vue
@@ -1040,6 +1040,7 @@ import Utils from "../../js/Utils";
import ElectronUtils from "../../js/ElectronUtils";
import DialogUtils from "../../js/DialogUtils";
import ToastUtils from "../../js/ToastUtils";
+import DownloadUtils from "../../js/DownloadUtils";
import GlobalEmitter from "../../js/GlobalEmitter";
export default {
name: "AboutPage",
@@ -1203,16 +1204,9 @@ export default {
try {
const downloadName = filename.endsWith(".zip") ? filename : `${filename}.zip`;
const response = await window.api.get(`/api/v1/database/snapshots/${filename}/download`, {
- responseType: "blob",
+ responseType: "arraybuffer",
});
- const url = window.URL.createObjectURL(new Blob([response.data], { type: "application/zip" }));
- const link = document.createElement("a");
- link.href = url;
- link.setAttribute("download", downloadName);
- document.body.appendChild(link);
- link.click();
- link.remove();
- window.URL.revokeObjectURL(url);
+ await DownloadUtils.downloadFromApiResponse(response, downloadName);
ToastUtils.success(this.$t("about.snapshot_downloaded"));
} catch {
ToastUtils.error(this.$t("about.snapshot_download_failed"));
@@ -1221,16 +1215,9 @@ export default {
async downloadBackupFile(filename) {
try {
const response = await window.api.get(`/api/v1/database/backups/${filename}/download`, {
- responseType: "blob",
+ responseType: "arraybuffer",
});
- const url = window.URL.createObjectURL(new Blob([response.data]));
- const link = document.createElement("a");
- link.href = url;
- link.setAttribute("download", filename);
- document.body.appendChild(link);
- link.click();
- link.remove();
- window.URL.revokeObjectURL(url);
+ await DownloadUtils.downloadFromApiResponse(response, filename);
ToastUtils.success(this.$t("about.backup_downloaded"));
} catch {
ToastUtils.error(this.$t("about.backup_download_failed"));
@@ -1392,20 +1379,12 @@ export default {
this.backupError = "";
try {
const response = await window.api.get("/api/v1/database/backup/download", {
- responseType: "blob",
+ responseType: "arraybuffer",
});
- const blob = new Blob([response.data], { type: "application/zip" });
- const url = window.URL.createObjectURL(blob);
- const link = document.createElement("a");
- link.href = url;
const filename =
response.headers["content-disposition"]?.split("filename=")?.[1]?.replace(/"/g, "") ||
"meshchatx-backup.zip";
- link.setAttribute("download", filename);
- document.body.appendChild(link);
- link.click();
- link.remove();
- window.URL.revokeObjectURL(url);
+ await DownloadUtils.downloadFromApiResponse(response, filename);
this.backupMessage = "Backup downloaded";
await this.getDatabaseHealth();
} catch (e) {

diff --git a/meshchatx/src/frontend/components/messages/ConversationMessageEntry.vue b/meshchatx/src/frontend/components/messages/ConversationMessageEntry.vue
index f14809fe..4c16e7cb 100644
--- a/meshchatx/src/frontend/components/messages/ConversationMessageEntry.vue
+++ b/meshchatx/src/frontend/components/messages/ConversationMessageEntry.vue
@@ -899,19 +899,17 @@
<!-- file attachment fields -->
<div v-if="cv.hasFileAttachments(chatItem.lxmf_message)" class="space-y-2 mt-1">
- <a
+ <button
v-for="(file_attachment, index) of chatItem.lxmf_message.fields?.file_attachments ?? []"
:key="file_attachment.file_name"
- target="_blank"
- :download="file_attachment.file_name"
- :href="`/api/v1/lxmf-messages/attachment/${chatItem.lxmf_message.hash}/file?file_index=${index}`"
- class="flex items-center gap-3 border rounded-lg px-3 py-2 text-sm font-medium cursor-pointer transition-colors"
+ type="button"
+ class="flex w-full items-center gap-3 border rounded-lg px-3 py-2 text-sm font-medium cursor-pointer transition-colors text-left"
:class="
chatItem.is_outbound
? cv.outboundEmbeddedCardClass(chatItem)
: 'bg-gray-50 dark:bg-zinc-800/50 text-gray-700 dark:text-zinc-300 border-gray-200/60 dark:border-zinc-700 hover:bg-gray-100 dark:hover:bg-zinc-800'
"
- @click.stop
+ @click.stop="cv.downloadLxmfFileAttachment(chatItem, index)"
>
<div class="my-auto">
<MaterialDesignIcon icon-name="paperclip" class="size-5" />
@@ -934,7 +932,7 @@
<div class="my-auto">
<MaterialDesignIcon icon-name="download" class="size-5" />
</div>
- </a>
+ </button>
</div>
<!-- commands -->

diff --git a/meshchatx/src/frontend/components/messages/ConversationViewer.vue b/meshchatx/src/frontend/components/messages/ConversationViewer.vue
index 2c8f1d46..2ff46185 100644
--- a/meshchatx/src/frontend/components/messages/ConversationViewer.vue
+++ b/meshchatx/src/frontend/components/messages/ConversationViewer.vue
@@ -1014,6 +1014,13 @@
<MaterialDesignIcon icon-name="code-json" class="size-4 text-gray-400" />
View Raw LXM
</ContextMenuItem>
+ <ContextMenuItem
+ v-if="messageContextMenu.chatItem?.lxmf_message?.fields?.image"
+ @click="downloadMessageImage(messageContextMenu.chatItem)"
+ >
+ <MaterialDesignIcon icon-name="download" class="size-4 text-blue-500" />
+ {{ $t("messages.save_image_to_device") }}
+ </ContextMenuItem>
<ContextMenuItem
v-if="messageContextMenu.chatItem?.lxmf_message?.fields?.image"
@click="saveMessageImageToStickers(messageContextMenu.chatItem)"
@@ -4983,6 +4990,51 @@ export default {
downloadFileFromBase64: async function (fileName, fileBytesBase64) {
DownloadUtils.downloadFromBase64(fileName, fileBytesBase64);
},
+ async downloadLxmfFileAttachment(chatItem, fileIndex) {
+ const msg = chatItem?.lxmf_message;
+ const attachments = msg?.fields?.file_attachments;
+ if (!msg?.hash || !Array.isArray(attachments) || fileIndex < 0 || fileIndex >= attachments.length) {
+ return;
+ }
+ const attachment = attachments[fileIndex];
+ const fileName = attachment.file_name || "download";
+ try {
+ const response = await window.api.get(`/api/v1/lxmf-messages/attachment/${msg.hash}/file`, {
+ params: { file_index: fileIndex },
+ responseType: "arraybuffer",
+ });
+ await DownloadUtils.downloadFromApiResponse(response, fileName);
+ } catch (e) {
+ console.error(e);
+ ToastUtils.error(this.$t("common.error"));
+ }
+ },
+ async downloadMessageImage(chatItem) {
+ this.messageContextMenu.show = false;
+ const msg = chatItem?.lxmf_message;
+ const img = msg?.fields?.image;
+ if (!msg?.hash || !img) {
+ return;
+ }
+ const rawType = String(img.image_type || "png")
+ .replace(/^image\//, "")
+ .toLowerCase();
+ const ext = rawType === "jpeg" ? "jpg" : rawType || "png";
+ const fileName = `image-${msg.hash.slice(0, 8)}.${ext}`;
+ try {
+ if (img.image_bytes) {
+ DownloadUtils.downloadFromBase64(fileName, img.image_bytes);
+ return;
+ }
+ const response = await window.api.get(`/api/v1/lxmf-messages/attachment/${msg.hash}/image`, {
+ responseType: "arraybuffer",
+ });
+ await DownloadUtils.downloadFromApiResponse(response, fileName);
+ } catch (e) {
+ console.error(e);
+ ToastUtils.error(this.$t("common.error"));
+ }
+ },
async processAudioForSelectedPeerChatItems() {
for (const chatItem of this.selectedPeerChatItems) {
// skip if no audio, or if audio bytes are missing (must be downloaded manually)

diff --git a/meshchatx/src/frontend/components/settings/IdentitiesPage.vue b/meshchatx/src/frontend/components/settings/IdentitiesPage.vue
index d104c034..3fc6ca7d 100644
--- a/meshchatx/src/frontend/components/settings/IdentitiesPage.vue
+++ b/meshchatx/src/frontend/components/settings/IdentitiesPage.vue
@@ -430,6 +430,7 @@ import MaterialDesignIcon from "../MaterialDesignIcon.vue";
import LxmfUserIcon from "../LxmfUserIcon.vue";
import ToastUtils from "../../js/ToastUtils";
import DialogUtils from "../../js/DialogUtils";
+import DownloadUtils from "../../js/DownloadUtils";
import GlobalEmitter from "../../js/GlobalEmitter";
export default {
@@ -504,17 +505,9 @@ export default {
async downloadIdentityFile() {
try {
const response = await window.api.get("/api/v1/identity/backup/download", {
- responseType: "blob",
+ responseType: "arraybuffer",
});
- const blob = new Blob([response.data], { type: "application/octet-stream" });
- const url = window.URL.createObjectURL(blob);
- const link = document.createElement("a");
- link.href = url;
- link.setAttribute("download", "identity");
- document.body.appendChild(link);
- link.click();
- link.remove();
- window.URL.revokeObjectURL(url);
+ await DownloadUtils.downloadFromApiResponse(response, "identity");
ToastUtils.success(this.$t("identities.identity_exported"));
} catch {
ToastUtils.error(this.$t("identities.identity_export_failed"));
@@ -537,16 +530,9 @@ export default {
async downloadAllIdentities() {
try {
const response = await window.api.get("/api/v1/identities/export-all", {
- responseType: "blob",
+ responseType: "arraybuffer",
});
- const url = window.URL.createObjectURL(new Blob([response.data], { type: "application/zip" }));
- const link = document.createElement("a");
- link.href = url;
- link.setAttribute("download", "identities_export.zip");
- document.body.appendChild(link);
- link.click();
- link.remove();
- window.URL.revokeObjectURL(url);
+ await DownloadUtils.downloadFromApiResponse(response, "identities_export.zip");
ToastUtils.success(this.$t("identities.export_all_success"));
} catch (e) {
const msg = e?.response?.data?.message || this.$t("identities.export_all_failed");

diff --git a/meshchatx/src/frontend/components/settings/SettingsPage.vue b/meshchatx/src/frontend/components/settings/SettingsPage.vue
index a9430ba4..428de7d0 100644
--- a/meshchatx/src/frontend/components/settings/SettingsPage.vue
+++ b/meshchatx/src/frontend/components/settings/SettingsPage.vue
@@ -4393,12 +4393,11 @@ export default {
try {
const response = await window.api.get("/api/v1/stickers/export");
const dataStr = JSON.stringify(response.data, null, 2);
- const dataUri = "data:application/json;charset=utf-8," + encodeURIComponent(dataStr);
const exportFileDefaultName = `meshchat_stickers_${new Date().toISOString().slice(0, 10)}.json`;
- const linkElement = document.createElement("a");
- linkElement.setAttribute("href", dataUri);
- linkElement.setAttribute("download", exportFileDefaultName);
- linkElement.click();
+ await DownloadUtils.downloadFile(
+ exportFileDefaultName,
+ new Blob([dataStr], { type: "application/json" })
+ );
ToastUtils.success(this.$t("stickers.export_done"));
} catch {
ToastUtils.error(this.$t("stickers.import_failed"));
@@ -4451,12 +4450,11 @@ export default {
try {
const response = await window.api.get("/api/v1/gifs/export");
const dataStr = JSON.stringify(response.data, null, 2);
- const dataUri = "data:application/json;charset=utf-8," + encodeURIComponent(dataStr);
const exportFileDefaultName = `meshchat_gifs_${new Date().toISOString().slice(0, 10)}.json`;
- const linkElement = document.createElement("a");
- linkElement.setAttribute("href", dataUri);
- linkElement.setAttribute("download", exportFileDefaultName);
- linkElement.click();
+ await DownloadUtils.downloadFile(
+ exportFileDefaultName,
+ new Blob([dataStr], { type: "application/json" })
+ );
ToastUtils.success(this.$t("gifs.export_done"));
} catch {
ToastUtils.error(this.$t("gifs.import_failed"));

diff --git a/meshchatx/src/frontend/js/DownloadUtils.js b/meshchatx/src/frontend/js/DownloadUtils.js
index 764a28c0..5799bda2 100644
--- a/meshchatx/src/frontend/js/DownloadUtils.js
+++ b/meshchatx/src/frontend/js/DownloadUtils.js
@@ -7,6 +7,34 @@ function isAndroidSaveBridge() {
}
class DownloadUtils {
+ static parseFilenameFromContentDisposition(header, defaultFilename) {
+ if (!header || typeof header !== "string") {
+ return defaultFilename;
+ }
+ const star = header.match(/filename\*=UTF-8''([^;\s]+)/i);
+ if (star?.[1]) {
+ try {
+ return decodeURIComponent(star[1]);
+ } catch {
+ // fall through
+ }
+ }
+ const plain = header.match(/filename="?([^";\n]+)"?/i);
+ if (plain?.[1]) {
+ return plain[1].trim();
+ }
+ return defaultFilename;
+ }
+
+ static async downloadFromApiResponse(response, defaultFilename) {
+ const headers = response?.headers || {};
+ const cd = headers["content-disposition"] || headers["Content-Disposition"];
+ const filename = DownloadUtils.parseFilenameFromContentDisposition(cd, defaultFilename);
+ const type = headers["content-type"] || headers["Content-Type"] || "application/octet-stream";
+ const blob = new Blob([response.data], { type });
+ await DownloadUtils.downloadFile(filename, blob);
+ }
+
static _blobToBase64(blob) {
return new Promise((resolve, reject) => {
const reader = new FileReader();

diff --git a/meshchatx/src/frontend/locales/de.json b/meshchatx/src/frontend/locales/de.json
index c92357d3..8229ccce 100644
--- a/meshchatx/src/frontend/locales/de.json
+++ b/meshchatx/src/frontend/locales/de.json
@@ -134,6 +134,27 @@
"privacy_data": "Daten & Gerät",
"privacy_subsection_device": "Dieses Gerät",
"privacy_subsection_telemetry": "Mesh-Telemetrie",
+ "privacy_mode_enabled": "Datenschutzmodus (externes HTTP/HTTPS blockieren)",
+ "privacy_mode_description": "Blockiert Kartenkacheln, Geocoding, LibreTranslate, Firmware-Downloads und andere ausgehende HTTP/HTTPS-Verbindungen der App. Die Content Security Policy des Browsers wird auf Same-Origin beschränkt.",
+ "web_exposure_title": "Netzwerkexposition",
+ "web_exposure_description": "Die Bind-Adresse der Web-UI wird beim Start über --host oder MESHCHAT_HOST festgelegt. Beschränken Sie den Zugriff, wenn nicht an Loopback gebunden.",
+ "web_listen_address": "Aktuelle Bind-Adresse",
+ "web_listen_https": "HTTPS",
+ "web_exposure_warning_title": "Diese Instanz ist über localhost hinaus erreichbar",
+ "web_exposure_warning_body": "Das Binden an eine Nicht-Loopback-Adresse macht die Web-UI in Ihrem Netzwerk erreichbar. Aktivieren Sie die Authentifizierung, schränken Sie den Port per Firewall ein und bevorzugen Sie den Zugriff über ein VPN, das Sie kontrollieren.",
+ "web_exposure_check_auth": "Authentifizierung aktiviert",
+ "web_exposure_check_auth_off": "Authentifizierung ist deaktiviert",
+ "web_exposure_check_firewall": "Ich habe den Portzugriff per Firewall oder Bind-Regeln eingeschränkt",
+ "web_exposure_check_vpn": "Fernzugriff nur über ein von mir kontrolliertes VPN",
+ "web_ui_ip_allowlist": "IP-Zulassungsliste der Web-UI",
+ "web_ui_ip_allowlist_description": "Optionale kommagetrennte IPs oder CIDR-Bereiche (z. B. 127.0.0.1/32, 192.168.1.0/24). Leer lässt alle Clients zu.",
+ "web_ui_ip_allowlist_placeholder": "127.0.0.1/32, ::1/128, 192.168.0.0/16",
+ "landlock_status": "Landlock-Sandbox (Linux)",
+ "landlock_active": "Aktiv",
+ "landlock_inactive": "Nicht aktiv",
+ "landlock_auto_enabled": "Auf diesem Linux-Kernel automatisch aktiviert",
+ "landlock_kernel_unsupported": "Kernel unterstützt kein Landlock (5.13+ erforderlich)",
+ "landlock_disabled_by_env": "Deaktiviert über MESHCHAT_LANDLOCK=0",
"settings_map_eyebrow": "Karte",
"messages_description": "Steuern Sie, wie MeshChat fehlgeschlagene Zustellungen wiederholt oder eskaliert. Kontrollieren Sie das automatische Wiederholungsverhalten, die erneute Übertragung von Anhängen und Fallback-Mechanismen, um eine zuverlässige Nachrichtenzustellung über das Mesh-Netzwerk zu gewährleisten.",
"auto_resend_title": "Automatisch erneut senden, wenn Peer ankündigt",

diff --git a/meshchatx/src/frontend/locales/en.json b/meshchatx/src/frontend/locales/en.json
index 4af4aa3c..90033575 100644
--- a/meshchatx/src/frontend/locales/en.json
+++ b/meshchatx/src/frontend/locales/en.json
@@ -1413,6 +1413,7 @@
"forward_content_stub": "_Forwarded via MeshChatX_\nAttachments and quoted text belong to the original sender.",
"forward_open_party_is_self": "That is your own address.",
"message_actions": "Message actions",
+ "save_image_to_device": "Save image to device",
"react": "React",
"reaction_you": "You",
"reaction_send_failed": "Could not send reaction",

diff --git a/meshchatx/src/frontend/locales/es.json b/meshchatx/src/frontend/locales/es.json
index f76a49bf..080b6f81 100644
--- a/meshchatx/src/frontend/locales/es.json
+++ b/meshchatx/src/frontend/locales/es.json
@@ -140,6 +140,27 @@
"privacy_data": "Datos y dispositivo",
"privacy_subsection_device": "Este dispositivo",
"privacy_subsection_telemetry": "Telemetría mesh",
+ "privacy_mode_enabled": "Modo de privacidad (bloquear HTTP/HTTPS externo)",
+ "privacy_mode_description": "Bloquea teselas de mapa, geocodificación, LibreTranslate, descargas de firmware y otras conexiones HTTP/HTTPS salientes de la app. La política de seguridad de contenido del navegador se restringe solo al mismo origen.",
+ "web_exposure_title": "Exposición de red",
+ "web_exposure_description": "La dirección de enlace de la interfaz web se establece al inicio con --host o MESHCHAT_HOST. Restrinja quién puede acceder cuando no esté enlazada a loopback.",
+ "web_listen_address": "Dirección de enlace actual",
+ "web_listen_https": "HTTPS",
+ "web_exposure_warning_title": "Esta instancia es accesible más allá de localhost",
+ "web_exposure_warning_body": "Enlazar a una dirección que no sea loopback expone la interfaz web en su red. Habilite la autenticación, restrinja el puerto con un firewall y prefiera el acceso remoto a través de una VPN que usted controle.",
+ "web_exposure_check_auth": "Autenticación habilitada",
+ "web_exposure_check_auth_off": "La autenticación está deshabilitada",
+ "web_exposure_check_firewall": "Restringí el acceso al puerto con un firewall o reglas de enlace",
+ "web_exposure_check_vpn": "El acceso remoto es solo a través de una VPN que controlo",
+ "web_ui_ip_allowlist": "Lista de IPs permitidas de la interfaz web",
+ "web_ui_ip_allowlist_description": "IPs o rangos CIDR opcionales separados por comas (por ejemplo 127.0.0.1/32, 192.168.1.0/24). Vacío permite todos los clientes.",
+ "web_ui_ip_allowlist_placeholder": "127.0.0.1/32, ::1/128, 192.168.0.0/16",
+ "landlock_status": "Sandbox Landlock (Linux)",
+ "landlock_active": "Activo",
+ "landlock_inactive": "No activo",
+ "landlock_auto_enabled": "Habilitado automáticamente en este kernel de Linux",
+ "landlock_kernel_unsupported": "El kernel no admite Landlock (se requiere 5.13+)",
+ "landlock_disabled_by_env": "Deshabilitado mediante MESHCHAT_LANDLOCK=0",
"settings_map_eyebrow": "Mapa",
"messages_description": "Configure cómo MeshChat maneja fallos de entrega de mensajes. Controle el comportamiento automático de retry, la retransmisión de archivos adjuntos y los mecanismos de retroceso para asegurar una entrega fiable de mensajes a través de la red de malla.",
"auto_resend_title": "Reenviamiento automático cuando el par anuncia",

diff --git a/meshchatx/src/frontend/locales/fi.json b/meshchatx/src/frontend/locales/fi.json
index eb9b9913..51daa7d0 100644
--- a/meshchatx/src/frontend/locales/fi.json
+++ b/meshchatx/src/frontend/locales/fi.json
@@ -141,6 +141,27 @@
"privacy_data": "Data & device",
"privacy_subsection_device": "This device",
"privacy_subsection_telemetry": "Mesh telemetry",
+ "privacy_mode_enabled": "Privacy mode (block external HTTP/HTTPS)",
+ "privacy_mode_description": "Blocks map tiles, geocoding, LibreTranslate, firmware downloads, and other outbound HTTP/HTTPS from the app. The browser Content Security Policy is tightened to same-origin only.",
+ "web_exposure_title": "Network exposure",
+ "web_exposure_description": "The web UI bind address is set at startup via --host or MESHCHAT_HOST. Restrict who can reach it when not bound to loopback.",
+ "web_listen_address": "Current bind address",
+ "web_listen_https": "HTTPS",
+ "web_exposure_warning_title": "This instance is reachable beyond localhost",
+ "web_exposure_warning_body": "Binding to a non-loopback address exposes the web UI on your network. Enable authentication, restrict the port with a firewall, and prefer access over a VPN you control.",
+ "web_exposure_check_auth": "Authentication enabled",
+ "web_exposure_check_auth_off": "Authentication is disabled",
+ "web_exposure_check_firewall": "I restricted port access with a firewall or bind rules",
+ "web_exposure_check_vpn": "Remote access is only via a VPN I control",
+ "web_ui_ip_allowlist": "Web UI IP allowlist",
+ "web_ui_ip_allowlist_description": "Optional comma-separated IPs or CIDR ranges (for example 127.0.0.1/32, 192.168.1.0/24). Empty allows all clients.",
+ "web_ui_ip_allowlist_placeholder": "127.0.0.1/32, ::1/128, 192.168.0.0/16",
+ "landlock_status": "Landlock sandbox (Linux)",
+ "landlock_active": "Active",
+ "landlock_inactive": "Not active",
+ "landlock_auto_enabled": "Enabled automatically on this Linux kernel",
+ "landlock_kernel_unsupported": "Kernel does not support Landlock (5.13+ required)",
+ "landlock_disabled_by_env": "Disabled via MESHCHAT_LANDLOCK=0",
"settings_map_eyebrow": "Map",
"messages_description": "Configure how MeshChat handles message delivery failures. Control automatic retry behavior, attachment retransmission, and fallback mechanisms to ensure reliable message delivery across the mesh network.",
"auto_resend_title": "Auto resend when peer announces",

diff --git a/meshchatx/src/frontend/locales/fr.json b/meshchatx/src/frontend/locales/fr.json
index c25cf722..2cb59a91 100644
--- a/meshchatx/src/frontend/locales/fr.json
+++ b/meshchatx/src/frontend/locales/fr.json
@@ -140,6 +140,27 @@
"privacy_data": "Données et appareil",
"privacy_subsection_device": "Cet appareil",
"privacy_subsection_telemetry": "Télémétrie mesh",
+ "privacy_mode_enabled": "Mode confidentialité (bloquer HTTP/HTTPS externe)",
+ "privacy_mode_description": "Bloque les tuiles de carte, le géocodage, LibreTranslate, les téléchargements de firmware et autres connexions HTTP/HTTPS sortantes de l'application. La politique de sécurité du contenu du navigateur est restreinte à same-origin uniquement.",
+ "web_exposure_title": "Exposition réseau",
+ "web_exposure_description": "L'adresse d'écoute de l'interface web est définie au démarrage via --host ou MESHCHAT_HOST. Restreignez l'accès lorsque l'écoute n'est pas sur loopback.",
+ "web_listen_address": "Adresse d'écoute actuelle",
+ "web_listen_https": "HTTPS",
+ "web_exposure_warning_title": "Cette instance est accessible au-delà de localhost",
+ "web_exposure_warning_body": "Lier à une adresse non loopback expose l'interface web sur votre réseau. Activez l'authentification, restreignez le port avec un pare-feu et privilégiez l'accès via un VPN que vous contrôlez.",
+ "web_exposure_check_auth": "Authentification activée",
+ "web_exposure_check_auth_off": "L'authentification est désactivée",
+ "web_exposure_check_firewall": "J'ai restreint l'accès au port avec un pare-feu ou des règles de liaison",
+ "web_exposure_check_vpn": "L'accès distant se fait uniquement via un VPN que je contrôle",
+ "web_ui_ip_allowlist": "Liste blanche IP de l'interface web",
+ "web_ui_ip_allowlist_description": "IPs ou plages CIDR optionnelles séparées par des virgules (par exemple 127.0.0.1/32, 192.168.1.0/24). Vide autorise tous les clients.",
+ "web_ui_ip_allowlist_placeholder": "127.0.0.1/32, ::1/128, 192.168.0.0/16",
+ "landlock_status": "Sandbox Landlock (Linux)",
+ "landlock_active": "Actif",
+ "landlock_inactive": "Inactif",
+ "landlock_auto_enabled": "Activé automatiquement sur ce noyau Linux",
+ "landlock_kernel_unsupported": "Le noyau ne prend pas en charge Landlock (5.13+ requis)",
+ "landlock_disabled_by_env": "Désactivé via MESHCHAT_LANDLOCK=0",
"settings_map_eyebrow": "Carte",
"messages_description": "Configurez comment MeshChat gère les échecs de livraison des messages. Contrôler le comportement de réessayer automatique, la retransmission des attaches et les mécanismes de repli pour assurer la livraison fiable des messages sur le réseau de mailles.",
"auto_resend_title": "Auto renverra quand pair annoncera",

diff --git a/meshchatx/src/frontend/locales/it.json b/meshchatx/src/frontend/locales/it.json
index b426d13e..dac5093e 100644
--- a/meshchatx/src/frontend/locales/it.json
+++ b/meshchatx/src/frontend/locales/it.json
@@ -140,6 +140,27 @@
"privacy_data": "Dati e dispositivo",
"privacy_subsection_device": "Questo dispositivo",
"privacy_subsection_telemetry": "Telemetria mesh",
+ "privacy_mode_enabled": "Modalità privacy (blocca HTTP/HTTPS esterni)",
+ "privacy_mode_description": "Blocca tile mappe, geocodifica, LibreTranslate, download firmware e altre connessioni HTTP/HTTPS in uscita dall'app. La Content Security Policy del browser è ristretta al solo same-origin.",
+ "web_exposure_title": "Esposizione di rete",
+ "web_exposure_description": "L'indirizzo di bind dell'interfaccia web è impostato all'avvio tramite --host o MESHCHAT_HOST. Limita chi può accedervi quando non è in ascolto su loopback.",
+ "web_listen_address": "Indirizzo di bind attuale",
+ "web_listen_https": "HTTPS",
+ "web_exposure_warning_title": "Questa istanza è raggiungibile oltre localhost",
+ "web_exposure_warning_body": "L'ascolto su un indirizzo non loopback espone l'interfaccia web sulla rete. Abilita l'autenticazione, limita la porta con un firewall e preferisci l'accesso remoto tramite una VPN sotto il tuo controllo.",
+ "web_exposure_check_auth": "Autenticazione abilitata",
+ "web_exposure_check_auth_off": "L'autenticazione è disabilitata",
+ "web_exposure_check_firewall": "Ho limitato l'accesso alla porta con un firewall o regole di bind",
+ "web_exposure_check_vpn": "L'accesso remoto avviene solo tramite una VPN sotto il mio controllo",
+ "web_ui_ip_allowlist": "Lista IP consentiti dell'interfaccia web",
+ "web_ui_ip_allowlist_description": "IP o intervalli CIDR opzionali separati da virgole (ad esempio 127.0.0.1/32, 192.168.1.0/24). Vuoto consente tutti i client.",
+ "web_ui_ip_allowlist_placeholder": "127.0.0.1/32, ::1/128, 192.168.0.0/16",
+ "landlock_status": "Sandbox Landlock (Linux)",
+ "landlock_active": "Attivo",
+ "landlock_inactive": "Non attivo",
+ "landlock_auto_enabled": "Abilitato automaticamente su questo kernel Linux",
+ "landlock_kernel_unsupported": "Il kernel non supporta Landlock (richiesto 5.13+)",
+ "landlock_disabled_by_env": "Disabilitato tramite MESHCHAT_LANDLOCK=0",
"settings_map_eyebrow": "Mappa",
"messages_description": "Configura come MeshChat gestisce i fallimenti di consegna dei messaggi. Controlla il comportamento dei tentativi automatici, la ritrasmissione degli allegati e i meccanismi di fallback per garantire una consegna affidabile dei messaggi nella rete mesh.",
"auto_resend_title": "Invia automaticamente quando il peer annuncia",

diff --git a/meshchatx/src/frontend/locales/nl.json b/meshchatx/src/frontend/locales/nl.json
index 22d6d381..1747b5fb 100644
--- a/meshchatx/src/frontend/locales/nl.json
+++ b/meshchatx/src/frontend/locales/nl.json
@@ -140,6 +140,27 @@
"privacy_data": "Data en apparaat",
"privacy_subsection_device": "Dit apparaat",
"privacy_subsection_telemetry": "Mesh-telemetrie",
+ "privacy_mode_enabled": "Privacymodus (blokkeer extern HTTP/HTTPS)",
+ "privacy_mode_description": "Blokkeert kaarttegels, geocodering, LibreTranslate, firmware-downloads en andere uitgaande HTTP/HTTPS-verbindingen van de app. Het Content Security Policy van de browser wordt beperkt tot same-origin.",
+ "web_exposure_title": "Netwerkblootstelling",
+ "web_exposure_description": "Het bind-adres van de web-UI wordt bij opstarten ingesteld via --host of MESHCHAT_HOST. Beperk wie toegang heeft wanneer niet aan loopback gebonden.",
+ "web_listen_address": "Huidig bind-adres",
+ "web_listen_https": "HTTPS",
+ "web_exposure_warning_title": "Deze instantie is bereikbaar buiten localhost",
+ "web_exposure_warning_body": "Binden aan een niet-loopback-adres maakt de web-UI bereikbaar op uw netwerk. Schakel authenticatie in, beperk de poort met een firewall en geef de voorkeur aan toegang via een VPN die u beheert.",
+ "web_exposure_check_auth": "Authenticatie ingeschakeld",
+ "web_exposure_check_auth_off": "Authenticatie is uitgeschakeld",
+ "web_exposure_check_firewall": "Ik heb poorttoegang beperkt met een firewall of bind-regels",
+ "web_exposure_check_vpn": "Externe toegang verloopt alleen via een VPN die ik beheer",
+ "web_ui_ip_allowlist": "IP-toegestane lijst web-UI",
+ "web_ui_ip_allowlist_description": "Optionele komma-gescheiden IP's of CIDR-bereiken (bijvoorbeeld 127.0.0.1/32, 192.168.1.0/24). Leeg staat alle clients toe.",
+ "web_ui_ip_allowlist_placeholder": "127.0.0.1/32, ::1/128, 192.168.0.0/16",
+ "landlock_status": "Landlock-sandbox (Linux)",
+ "landlock_active": "Actief",
+ "landlock_inactive": "Niet actief",
+ "landlock_auto_enabled": "Automatisch ingeschakeld op deze Linux-kernel",
+ "landlock_kernel_unsupported": "Kernel ondersteunt geen Landlock (5.13+ vereist)",
+ "landlock_disabled_by_env": "Uitgeschakeld via MESHCHAT_LANDLOCK=0",
"settings_map_eyebrow": "Kaart",
"messages_description": "Configureer hoe MeshChat omgaat met foutieve berichtlevering. Controle automatische retry gedrag, bijlage doorgifte, en terugvalmechanismen om betrouwbare bericht levering over het mesh netwerk te garanderen.",
"auto_resend_title": "Automatisch opnieuw verzenden wanneer peer aankondigt",

diff --git a/meshchatx/src/frontend/locales/ru.json b/meshchatx/src/frontend/locales/ru.json
index b96d6912..88c9f232 100644
--- a/meshchatx/src/frontend/locales/ru.json
+++ b/meshchatx/src/frontend/locales/ru.json
@@ -134,6 +134,27 @@
"privacy_data": "Данные и устройство",
"privacy_subsection_device": "Это устройство",
"privacy_subsection_telemetry": "Телеметрия mesh",
+ "privacy_mode_enabled": "Режим конфиденциальности (блокировка внешнего HTTP/HTTPS)",
+ "privacy_mode_description": "Блокирует тайлы карт, геокодирование, LibreTranslate, загрузку прошивок и другие исходящие HTTP/HTTPS-соединения приложения. Политика безопасности контента браузера ограничивается только same-origin.",
+ "web_exposure_title": "Сетевая доступность",
+ "web_exposure_description": "Адрес привязки веб-интерфейса задаётся при запуске через --host или MESHCHAT_HOST. Ограничьте доступ, если привязка не к loopback.",
+ "web_listen_address": "Текущий адрес привязки",
+ "web_listen_https": "HTTPS",
+ "web_exposure_warning_title": "Этот экземпляр доступен за пределами localhost",
+ "web_exposure_warning_body": "Привязка к адресу, отличному от loopback, открывает веб-интерфейс в вашей сети. Включите аутентификацию, ограничьте порт файрволом и предпочитайте доступ через VPN под вашим контролем.",
+ "web_exposure_check_auth": "Аутентификация включена",
+ "web_exposure_check_auth_off": "Аутентификация отключена",
+ "web_exposure_check_firewall": "Я ограничил доступ к порту файрволом или правилами привязки",
+ "web_exposure_check_vpn": "Удалённый доступ только через VPN под моим контролем",
+ "web_ui_ip_allowlist": "Список разрешённых IP веб-интерфейса",
+ "web_ui_ip_allowlist_description": "Необязательные IP или диапазоны CIDR через запятую (например 127.0.0.1/32, 192.168.1.0/24). Пустое значение разрешает всех клиентов.",
+ "web_ui_ip_allowlist_placeholder": "127.0.0.1/32, ::1/128, 192.168.0.0/16",
+ "landlock_status": "Песочница Landlock (Linux)",
+ "landlock_active": "Активна",
+ "landlock_inactive": "Не активна",
+ "landlock_auto_enabled": "Автоматически включена в этом ядре Linux",
+ "landlock_kernel_unsupported": "Ядро не поддерживает Landlock (требуется 5.13+)",
+ "landlock_disabled_by_env": "Отключено через MESHCHAT_LANDLOCK=0",
"settings_map_eyebrow": "Карта",
"messages_description": "Настройте, как MeshChat повторяет или эскалирует неудачные доставки. Контролируйте автоматический повтор, пересылку вложений и механизмы отката для обеспечения надежной доставки сообщений в сети mesh.",
"auto_resend_title": "Автоповтор при анонсе узла",

diff --git a/meshchatx/src/frontend/locales/zh.json b/meshchatx/src/frontend/locales/zh.json
index f8bfcaec..aa3a749a 100644
--- a/meshchatx/src/frontend/locales/zh.json
+++ b/meshchatx/src/frontend/locales/zh.json
@@ -140,6 +140,27 @@
"privacy_data": "数据与本机",
"privacy_subsection_device": "本机",
"privacy_subsection_telemetry": "Mesh 遥测",
+ "privacy_mode_enabled": "隐私模式(阻止外部 HTTP/HTTPS)",
+ "privacy_mode_description": "阻止地图瓦片、地理编码、LibreTranslate、固件下载以及应用的其他出站 HTTP/HTTPS 连接。浏览器内容安全策略收紧为仅同源。",
+ "web_exposure_title": "网络暴露",
+ "web_exposure_description": "Web UI 绑定地址在启动时通过 --host 或 MESHCHAT_HOST 设置。未绑定到 loopback 时,请限制可访问者。",
+ "web_listen_address": "当前绑定地址",
+ "web_listen_https": "HTTPS",
+ "web_exposure_warning_title": "此实例可在 localhost 之外访问",
+ "web_exposure_warning_body": "绑定到非 loopback 地址会在您的网络上暴露 Web UI。请启用身份验证、用防火墙限制端口,并优先通过您控制的 VPN 访问。",
+ "web_exposure_check_auth": "已启用身份验证",
+ "web_exposure_check_auth_off": "身份验证已禁用",
+ "web_exposure_check_firewall": "我已用防火墙或绑定规则限制端口访问",
+ "web_exposure_check_vpn": "远程访问仅通过我控制的 VPN",
+ "web_ui_ip_allowlist": "Web UI IP 允许列表",
+ "web_ui_ip_allowlist_description": "可选的逗号分隔 IP 或 CIDR 范围(例如 127.0.0.1/32、192.168.1.0/24)。留空允许所有客户端。",
+ "web_ui_ip_allowlist_placeholder": "127.0.0.1/32, ::1/128, 192.168.0.0/16",
+ "landlock_status": "Landlock 沙箱(Linux)",
+ "landlock_active": "已启用",
+ "landlock_inactive": "未启用",
+ "landlock_auto_enabled": "在此 Linux 内核上自动启用",
+ "landlock_kernel_unsupported": "内核不支持 Landlock(需要 5.13+)",
+ "landlock_disabled_by_env": "已通过 MESHCHAT_LANDLOCK=0 禁用",
"settings_map_eyebrow": "地图",
"messages_description": "配置 MeshChat 如何处理消息发送失败。控制自动重试行为、附件重传和回退机制,以确保通过网格网络可靠地发送消息。",
"auto_resend_title": "当节点广播时自动重发",

diff --git a/tests/frontend/DownloadUtils.test.js b/tests/frontend/DownloadUtils.test.js
index 53d21f65..3780fa0d 100644
--- a/tests/frontend/DownloadUtils.test.js
+++ b/tests/frontend/DownloadUtils.test.js
@@ -25,4 +25,25 @@ describe("DownloadUtils", () => {
expect(typeof b64).toBe("string");
expect(b64.length).toBeGreaterThan(0);
});
+
+ it("parseFilenameFromContentDisposition prefers UTF-8 filename*", () => {
+ const header = "attachment; filename=\"fallback.bin\"; filename*=UTF-8''photo%2Epng";
+ expect(DownloadUtils.parseFilenameFromContentDisposition(header, "default.bin")).toBe("photo.png");
+ });
+
+ it("downloadFromApiResponse uses Android bridge when present", async () => {
+ const saveDownload = vi.fn();
+ window.MeshChatXAndroid = { saveDownload };
+ await DownloadUtils.downloadFromApiResponse(
+ {
+ data: new Uint8Array([9, 8, 7]),
+ headers: {
+ "content-disposition": 'attachment; filename="backup.zip"',
+ "content-type": "application/zip",
+ },
+ },
+ "fallback.zip"
+ );
+ expect(saveDownload).toHaveBeenCalledWith("backup.zip", expect.any(String));
+ });
});


──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────